package cn.sciento.transfer.infra.liquibase.parser;

import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Properties;
import liquibase.ContextExpression;
import liquibase.Labels;
import liquibase.changelog.ChangeLogParameters;
import liquibase.changelog.DatabaseChangeLog;
import liquibase.exception.ChangeLogParseException;
import liquibase.logging.LogService;
import liquibase.logging.LogType;
import liquibase.logging.Logger;
import liquibase.parser.ChangeLogParser;
import liquibase.parser.LiquibaseParser;
import liquibase.parser.core.ParsedNode;
import liquibase.resource.ResourceAccessor;
import liquibase.util.StreamUtil;
import cn.sciento.transfer.infra.liquibase.parser.data.DatabaseChangeLogFileData;
import org.springframework.util.StringUtils;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.BaseConstructor;
import org.yaml.snakeyaml.constructor.SafeConstructor;

public class YamlChangeLogInMemoryParser implements LiquibaseParser, ChangeLogParser {
  protected Logger log = LogService.getLog(getClass());
  
  public boolean supports(String changeLogFile, ResourceAccessor resourceAccessor) {
    for (String extension : getSupportedFileExtensions()) {
      if (changeLogFile.toLowerCase().endsWith("." + extension))
        return true; 
    } 
    return false;
  }
  
  protected String[] getSupportedFileExtensions() {
    return new String[] { "yaml-im", "yml-im" };
  }
  
  public int getPriority() {
    return 1;
  }
  
  public DatabaseChangeLog parse(String yamlFileJson, ChangeLogParameters changeLogParameters, ResourceAccessor resourceAccessor) throws ChangeLogParseException {
    if (StringUtils.isEmpty(yamlFileJson))
      throw new ChangeLogParseException("Empty content " + yamlFileJson); 
    DatabaseChangeLogFileData databaseChangeLogFileData = parseYamlFile(yamlFileJson);
    String fileName = databaseChangeLogFileData.getFileName();
    String fileContent = databaseChangeLogFileData.getFileContent();
    Yaml yaml = new Yaml((BaseConstructor)new SafeConstructor());
    try {
      InputStream changeLogStream = new ByteArrayInputStream(fileContent.getBytes(StandardCharsets.UTF_8));
      Map parsedYaml = parseYamlStream(fileContent, yaml, changeLogStream);
      if (parsedYaml == null || parsedYaml.isEmpty())
        throw new ChangeLogParseException("Empty content " + fileContent); 
      DatabaseChangeLog changeLog = new DatabaseChangeLog(fileName);
      Object rootList = parsedYaml.get("databaseChangeLog");
      if (rootList == null)
        throw new ChangeLogParseException("Could not find databaseChangeLog node"); 
      if (!(rootList instanceof List))
        throw new ChangeLogParseException("databaseChangeLog does not contain a list of entries. Each changeSet must begin ' - changeSet:'"); 
      for (Object obj :(List) rootList) {
        if (obj instanceof Map && ((Map)obj).containsKey("property")) {
          Map property = (Map)((Map)obj).get("property");
          ContextExpression context = new ContextExpression((String)property.get("context"));
          Labels labels = new Labels((String)property.get("labels"));
          Boolean global = getGlobalParam(property);
          if (property.containsKey("name")) {
            Object value = property.get("value");
            if (value != null)
              value = value.toString(); 
            changeLogParameters.set((String)property.get("name"), (String)value, context, labels, (String)property.get("dbms"), global.booleanValue(), changeLog);
            continue;
          } 
          if (property.containsKey("file"))
            loadChangeLogParametersFromFile(changeLogParameters, resourceAccessor, changeLog, property, context, labels, global); 
        } 
      } 
      replaceParameters(parsedYaml, changeLogParameters, changeLog);
      changeLog.setChangeLogParameters(changeLogParameters);
      ParsedNode databaseChangeLogNode = new ParsedNode(null, "databaseChangeLog");
      databaseChangeLogNode.setValue(rootList);
      changeLog.load(databaseChangeLogNode, resourceAccessor);
      return changeLog;
    } catch (ChangeLogParseException e) {
      throw e;
    } catch (Exception e) {
      throw new ChangeLogParseException("Error parsing " + fileContent, e);
    } 
  }
  
  private Map parseYamlStream(String physicalChangeLogLocation, Yaml yaml, InputStream changeLogStream) throws ChangeLogParseException {
    Map parsedYaml;
    try {
      parsedYaml = (Map)yaml.load(changeLogStream);
    } catch (Exception e) {
      throw new ChangeLogParseException("Syntax error in file " + physicalChangeLogLocation + ": " + e.getMessage(), e);
    } 
    return parsedYaml;
  }
  
  private void loadChangeLogParametersFromFile(ChangeLogParameters changeLogParameters, ResourceAccessor resourceAccessor, DatabaseChangeLog changeLog, Map property, ContextExpression context, Labels labels, Boolean global) throws IOException {
    Properties props = new Properties();
    try (InputStream propertiesStream = StreamUtil.singleInputStream((String)property
          .get("file"), resourceAccessor)) {
      if (propertiesStream == null) {
        this.log.info(LogType.LOG, "Could not open properties file " + property.get("file"));
      } else {
        props.load(propertiesStream);
        for (Map.Entry<Object, Object> entry : props.entrySet())
          changeLogParameters.set(entry.getKey().toString(), entry.getValue().toString(), context, labels, (String)property.get("dbms"), global.booleanValue(), changeLog); 
      } 
    } 
  }
  
  private Boolean getGlobalParam(Map property) {
    Boolean global = null;
    Object globalObj = property.get("global");
    if (globalObj == null) {
      global = Boolean.valueOf(true);
    } else {
      global = (Boolean)globalObj;
    } 
    return global;
  }
  
  protected void replaceParameters(Object obj, ChangeLogParameters changeLogParameters, DatabaseChangeLog changeLog) {
    if (obj instanceof Map) {
      for (Object entry1 : ((Map)obj).entrySet()) {
        Map.Entry entry = (Map.Entry) entry1;
        if (entry.getValue() instanceof Map || entry.getValue() instanceof java.util.Collection) {
          replaceParameters(entry.getValue(), changeLogParameters, changeLog);
          continue;
        } 
        if (entry.getValue() instanceof String)
          entry.setValue(changeLogParameters.expandExpressions((String)entry.getValue(), changeLog)); 
      } 
    } else if (obj instanceof java.util.Collection) {
      ListIterator<String> iterator = ((List)obj).listIterator();
      while (iterator.hasNext()) {
        Object child = iterator.next();
        if (child instanceof Map || child instanceof java.util.Collection) {
          replaceParameters(child, changeLogParameters, changeLog);
          continue;
        } 
        if (child instanceof String)
          iterator.set(changeLogParameters.expandExpressions((String)child, changeLog)); 
      } 
    } 
  }
  
  private DatabaseChangeLogFileData parseYamlFile(String yamlFile) throws ChangeLogParseException {
    if (StringUtils.isEmpty(yamlFile))
      throw new ChangeLogParseException("Empty content " + yamlFile); 
    DatabaseChangeLogFileData databaseChangeLogFileData = null;
    try {
      databaseChangeLogFileData = (DatabaseChangeLogFileData)(new ObjectMapper()).readValue(yamlFile, DatabaseChangeLogFileData.class);
    } catch (Exception ex) {
      throw new ChangeLogParseException("Parse Yaml file failed ", ex);
    } 
    return databaseChangeLogFileData;
  }
}
